/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springsource.loaded.agent;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springsource.loaded.Constants;
import org.springsource.loaded.GlobalConfiguration;
import org.springsource.loaded.IsReloadableTypePlugin;
import org.springsource.loaded.LoadtimeInstrumentationPlugin;
import org.springsource.loaded.Log;
import org.springsource.loaded.Plugin;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.SystemClassReflectionInvestigator;
import org.springsource.loaded.SystemClassReflectionRewriter;
import org.springsource.loaded.SystemClassReflectionRewriter.RewriteResult;
import org.springsource.loaded.TypeRegistry;
import org.springsource.loaded.TypeRegistry.ReloadableTypeNameDecision;
import org.springsource.loaded.Utils;
import org.springsource.loaded.ri.ReflectiveInterceptor;
import org.springsource.loaded.support.Java8;

/**
 * The entry point for the agent - all classes that can be modified will be passed into preProcess(). They have to be
 * dealt with in one of these ways:
 * <ul>
 * <li>reloadable types need their bytecode rewriting so that they can be modified later
 * <li>'framework' types (not loaded by the system classloader) need their reflection calls rewritten
 * <li>system classes also need their reflection calls modified but in a different way (they cannot have dependencies on
 * types they cannot see)
 * </ul>
 *
 * @author Andy Clement
 * @since 0.5.0
 */
public class SpringLoadedPreProcessor implements Constants {

	private static Logger log = Logger.getLogger(SpringLoadedPreProcessor.class.getName());

	private static List<Plugin> plugins = null;

	// Global control to turn off the agent, used when testing
	public static boolean disabled = false;

	// These are system classes that contain reflection code and so need instrumenting when encountered.
	private static List<String> systemClassesContainingReflection;

	// Once the system classes have been encountered and instrumented, they need initialization once they have been defined
	// to the VM.  This records the list of those that have not yet been initialized.
	private Map<String, Integer> systemClassesRequiringInitialization = new HashMap<String, Integer>();

	// Once the first reloadabletype is hit, we can start initializing the system classes with reflective interceptors.
	// Doing it early can lead to hangs
	private static boolean firstReloadableTypeHit = false;

	public void initialize() {
		// When spring loaded is running as an agent, it should not be defining types directly (this setting does not apply to
		// the generated suuport types)
		GlobalConfiguration.directlyDefineTypes = false;
		GlobalConfiguration.fileSystemMonitoring = true;
		systemClassesContainingReflection = new ArrayList<String>();
		// So that jaxb annotations will cause discovery of the correct properties:
		systemClassesContainingReflection.add("com/sun/xml/internal/bind/v2/model/nav/ReflectionNavigator");
		// So that proxies are generated with the right set of methods inside
		systemClassesContainingReflection.add("sun/misc/ProxyGenerator");
		// (at least) the call to getModifiers() needs interception
		systemClassesContainingReflection.add("java/lang/reflect/Proxy");
		// So that javabeans introspection is intercepter
		systemClassesContainingReflection.add("java/beans/Introspector");

		// Related to serialization
		// TODO [serialization] Caches in ObjectStreamClass for descriptors, need clearing on reload
		systemClassesContainingReflection.add("java/io/ObjectStreamClass");
		systemClassesContainingReflection.add("java/io/ObjectStreamClass$EntryFuture");

		// Don't need this right now, instead we are not removing 'final' from the serialVersionUID
		//		// Need to catch at least the call to access the serialVersionUID made in getDeclaredSUID()
		//		systemClassesContainingReflection.add("java/io/ObjectStreamClass$2");
	}

	/**
	 * Main entry point to Spring Loaded when it is running as an agent. This method will use the classLoader and the
	 * class name in order to determine whether the type should be made reloadable. Non-reloadable types will at least
	 * get their call sites rewritten.
	 *
	 * @param classLoader the classloader loading this type
	 * @param slashedClassName the slashed class name (e.g. java/lang/String) being loaded
	 * @param protectionDomain the protection domain for the loaded class
	 * @param bytes the class bytes for the class being loaded
	 * @return potentially modified bytes
	 */
	public byte[] preProcess(ClassLoader classLoader, String slashedClassName, ProtectionDomain protectionDomain,
			byte[] bytes) {
		if (disabled) {
			return bytes;
		}

		// TODO need configurable debug here, ability to dump any code before/after
		for (Plugin plugin : getGlobalPlugins()) {
			if (plugin instanceof LoadtimeInstrumentationPlugin) {
				LoadtimeInstrumentationPlugin loadtimeInstrumentationPlugin = (LoadtimeInstrumentationPlugin) plugin;
				if (loadtimeInstrumentationPlugin.accept(slashedClassName, classLoader, protectionDomain, bytes)) {
					bytes = loadtimeInstrumentationPlugin.modify(slashedClassName, classLoader, bytes);
				}
			}
		}

		tryToEnsureSystemClassesInitialized(slashedClassName);

		TypeRegistry typeRegistry = TypeRegistry.getTypeRegistryFor(classLoader);

		if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) {
			logPreProcess(classLoader, slashedClassName, typeRegistry);
		}

		if (typeRegistry == null) { // A null type registry indicates nothing is being made reloadable for the classloader
			if (classLoader == null && slashedClassName != null) { // Indicates loading of a system class
				if (systemClassesContainingReflection.contains(slashedClassName)) {
					try {
						// TODO [perf] why are we not using the cache here, is it because the list is so short?
						RewriteResult rr = SystemClassReflectionRewriter.rewrite(slashedClassName, bytes);
						if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) {
							log.info("System class rewritten: name=" + slashedClassName + " rewrite summary="
									+ rr.summarize());
						}
						systemClassesRequiringInitialization.put(slashedClassName, rr.bits);
						return rr.bytes;
					}
					catch (Exception re) {
						re.printStackTrace();
					}

				}
				else if (slashedClassName.equals("java/lang/invoke/InnerClassLambdaMetafactory")) {
					bytes = Java8.enhanceInnerClassLambdaMetaFactory(bytes);
					return bytes;
				}
				else if ((GlobalConfiguration.investigateSystemClassReflection
						|| GlobalConfiguration.rewriteAllSystemClasses)
						&&
						SystemClassReflectionInvestigator.investigate(slashedClassName, bytes,
								GlobalConfiguration.investigateSystemClassReflection) > 0) {
					// This block can help when you suspect there is a system class using reflection and that
					// class isn't on the 'shortlist' (in systemClassesContainingReflection). Basically turn on the
					// options to trigger this investigation then add them to the shortlist if it looks like they need rewriting.
					RewriteResult rr = SystemClassReflectionRewriter.rewrite(slashedClassName, bytes);
					if (GlobalConfiguration.rewriteAllSystemClasses) {
						systemClassesRequiringInitialization.put(slashedClassName, rr.bits);
						return rr.bytes;
					}
					else {
						System.err.println("Type " + slashedClassName + " rewrite summary: " + rr.summarize());
						return bytes;
					}
				}
			}
			return bytes;
		}

		// What happens here? The aim is to determine if the type should be made reloadable.
		// 1. If NO, but something in this classloader might be, then rewrite the call sites.
		// 2. If NO, and nothing in this classloader might be, return the original bytes.
		// 3. If YES, make the type reloadable (including rewriting call sites)

		ReloadableTypeNameDecision isReloadableTypeName = typeRegistry.isReloadableTypeName(slashedClassName,
				protectionDomain, bytes);

		if (isReloadableTypeName.isReloadable && GlobalConfiguration.explainMode && log.isLoggable(Level.INFO)) {
			log.info("[explanation] Based on the name, type " + slashedClassName + " is considered to be reloadable");
		}

		// logging causes a ClassCircularity problem when reporting on:
		// SL: Type 'org/codehaus/groovy/grails/cli/logging/GrailsConsolePrintStream' is not being made reloadable
		//		if (GlobalConfiguration.verboseMode && isReloadableTypeName) {
		//			Log.log("Type '"+slashedClassName+"' is preliminarily being considered a reloadable type");
		//		}
		if (isReloadableTypeName.isReloadable) {
			if (!firstReloadableTypeHit) {
				firstReloadableTypeHit = true;
				// TODO move into the ctor for ReloadableType so that it can't block loading
				tryToEnsureSystemClassesInitialized(slashedClassName);
			}

			if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
				log.info("processing " + slashedClassName + " as a reloadable type");
			}

			try {
				// TODO decide one way or the other on slashed/dotted from preprocessor to infrastructure
				String dottedClassName = slashedClassName.replace('/', '.');
				String watchPath = getWatchPathFromProtectionDomain(protectionDomain, slashedClassName);
				if (watchPath == null) {
					// For a CGLIB generated type, we may still need to make the type reloadable.  For example:
					// type: com/vmware/rabbit/ApplicationContext$$EnhancerByCGLIB$$512eb60c
					// codesource determined to be: file:/Users/aclement/springsource/tc-server-developer-2.1.1.RELEASE/spring-insight-instance/wtpwebapps/hello-rabbit-client/WEB-INF/lib/cglib-nodep-2.2.jar <no signer certificates>
					// But if the type 'com/vmware/rabbit/ApplicationContext' is reloadable, then this should be too
					boolean makeReloadableAnyway = false;
					int cglibIndex = slashedClassName.indexOf("$$EnhancerBy");

					if (cglibIndex != -1) {
						String originalType = slashedClassName.substring(0, cglibIndex);
						if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
							log.info("Appears to be a CGLIB type, checking if type " + originalType + " is reloadable");
						}
						TypeRegistry currentRegistry = typeRegistry;
						while (currentRegistry != null) {
							ReloadableType originalReloadable = currentRegistry.getReloadableType(originalType);
							if (originalReloadable != null) {
								makeReloadableAnyway = true;
								break;
							}
							currentRegistry = currentRegistry.getParentRegistry();
						}
						//						if (typeRegistry.isReloadableTypeName(originalType)) {
						//							if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
						//								log.info("Type " + originalType + " is reloadable, so making CGLIB type " + slashedClassName
						//										+ " reloadable");
						//							}
						//							makeReloadableAnyway = true;
						//						}
					}

					int cglibIndex2 = makeReloadableAnyway ? -1 : slashedClassName.indexOf("$$FastClassByCGLIB");
					if (cglibIndex2 != -1) {
						String originalType = slashedClassName.substring(0, cglibIndex2);
						if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
							log.info("Appears to be a CGLIB FastClass type, checking if type " + originalType
									+ " is reloadable");
						}
						TypeRegistry currentRegistry = typeRegistry;
						while (currentRegistry != null) {
							ReloadableType originalReloadable = currentRegistry.getReloadableType(originalType);
							if (originalReloadable != null) {
								makeReloadableAnyway = true;
								break;
							}
							currentRegistry = currentRegistry.getParentRegistry();
						}
						//						if (typeRegistry.isReloadableTypeName(originalType)) {
						//							if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
						//								log.info("Type " + originalType + " is reloadable, so making CGLIB type " + slashedClassName
						//										+ " reloadable");
						//							}
						//							makeReloadableAnyway = true;
						//						}
					}

					int proxyIndex = makeReloadableAnyway ? -1 : slashedClassName.indexOf("$Proxy");
					if (proxyIndex == 0 || (proxyIndex > 0 && slashedClassName.charAt(proxyIndex - 1) == '/')) {
						// Determine if the interfaces being implemented are reloadable
						String[] interfacesImplemented = Utils.discoverInterfaces(bytes);
						if (interfacesImplemented != null) {
							for (int i = 0; i < interfacesImplemented.length; i++) {
								TypeRegistry currentRegistry = typeRegistry;
								while (currentRegistry != null) {
									ReloadableType originalReloadable = currentRegistry.getReloadableType(
											interfacesImplemented[i]);
									if (originalReloadable != null) {
										makeReloadableAnyway = true;
										break;
									}
									currentRegistry = currentRegistry.getParentRegistry();
								}
								//								if (typeRegistry.isReloadableTypeName(interfacesImplemented[i])) {
								//									makeReloadableAnyway = true;
								//								}
							}
						}
					}
					// GRAILS-8098
					// The scaffolding loader will load stuff in this innerloader - if we don't make the types in it reloadable then they will clash
					// with the original (ordinary version) controller loaded by URLClassLoader (e.g. in an istcheck for some type we will
					// not find it in the InnerClassLoader, but find it in the super classloader, and it'll be the wrong one).
					// I wonder if the more general rule should be that
					// all classloaders below one loading reloadable stuff should also load reloadable stuff.
					if (!makeReloadableAnyway
							&& classLoader.getClass().getName().endsWith("GroovyClassLoader$InnerLoader")) {
						makeReloadableAnyway = true;
					}

					if (!makeReloadableAnyway) {
						// can't watch it for updates (it comes from a jar perhaps) so just rewrite call sites and return

						// Not planning to watch this class so ordinarily do not make it reloadable. UNLESS the user
						// is specifying that it needs to be. This may happen with split packages - some classes in a jar
						// and some on disk. During type rewriting the top most reloadable types get fields inserted -
						// when split across jars we get confused by split packages.  If we go by name (as the code does
						// right now) then we think we aren't the top most reloadable type but we don't realize that the
						// type above us comes from a jar. Hence this condition below. If the user did explicitly
						// specify types with this kind of name should be made reloadable we even make the ones from
						// the jar reloadable. (TODO: optimization, make a smarter isTopMostReloadableType test that
						// allows us to keep the jar loaded types as non reloadable).
						//						if (isReloadableTypeName.extraInfo && isReloadableTypeName.explicitlyIncluded
						//								&& !GlobalConfiguration.InTestMode) {
						//
						//						}
						//						else {
						if (GlobalConfiguration.verboseMode) {
							Log.log("Cannot watch " + slashedClassName + ": not making it reloadable");
						}
						if (needsClientSideRewriting(slashedClassName)) {
							bytes = typeRegistry.methodCallRewriteUseCacheIfAvailable(slashedClassName, bytes);
						}
						return bytes;
						//						}
					}
				}
				ReloadableType rtype = typeRegistry.addType(dottedClassName, bytes);
				if (rtype == null && GlobalConfiguration.callsideRewritingOn) {
					// it is not a candidate for being made reloadable (maybe it is an annotation type)
					// but we still need to rewrite call sites.
					bytes = typeRegistry.methodCallRewrite(bytes);
				}
				else {
					if (GlobalConfiguration.fileSystemMonitoring && watchPath != null) {
						typeRegistry.monitorForUpdates(rtype, watchPath);
					}
					return rtype.bytesLoaded;
				}
			}
			catch (RuntimeException re) {
				log.throwing("SpringLoadedPreProcessor", "preProcess", re);
				throw re;
			}
		}
		else {
			try {
				// TODO what happens across classloader boundaries? (for regular code and reflective calls)
				// Skipping the CallSiteClassLoader here because types from there will already have been dealt
				// with due to GroovyPlugin class that intercepts define in that infrastructure
				if (needsClientSideRewriting(slashedClassName) &&
						(classLoader == null || !classLoader.getClass().getName().equals(
								"org.codehaus.groovy.runtime.callsite.CallSiteClassLoader"))) {
					bytes = typeRegistry.methodCallRewriteUseCacheIfAvailable(slashedClassName, bytes);
				}
			}
			catch (Throwable t) {
				log.log(Level.SEVERE, "Unexpected problem transforming call sites", t);
			}
		}
		return bytes;
	}

	private void tryToEnsureSystemClassesInitialized(String slashedClassName) {
		if (firstReloadableTypeHit && !systemClassesRequiringInitialization.isEmpty()) {
			int lastSlash = slashedClassName.lastIndexOf('/');
			String pkg = lastSlash == -1 ? null : slashedClassName.substring(0, lastSlash);
			ensurePreparedForInjection();
			List<String> toRemoveList = new ArrayList<String>();
			for (Map.Entry<String, Integer> me : systemClassesRequiringInitialization.entrySet()) {
				String classname = me.getKey();
				// A ClassCircularityError can occur in the injectReflectiveInterceptorMethods() method below.  Reason:
				// ===
				// CCE: "A class or interface could not be loaded because it would be its own superclass or superinterface"
				// according to the Java Virtual Machine Specification (JVMS 2.17.2).
				// The implementation of the virtual machine generally detects this by noting the
				// beginning of an attempt to load a class and then noticing when the
				// same task thread attempts to load that same class again before the original
				// attempt has completed (is still in progress).
				// ===
				// So, if we attempt to 'fix up' a class here which has a relationship with the type we are currently
				// loading, then it looks like a CCE.  The crude initial fix is to avoid working on anything in the
				// same package as us.  This doesn't quite fix all the cases of course but addresses a chunk of them.
				// One remaining case I can clearly see in the log is that java.beans.Introspector (which needs fixing up)
				// uses a field of type com.sun.beans.WeakCache.

				// A full list of the special relationships could be encoded here (don't touch X until Y,Z,etc loaded)
				// but that will just get out of date so quickly.  Given that it isn't necessarily a problem because
				// the fixing up will be re-attempted again, the simplest thing would be just to avoid printing
				// CCEs (but log all other issues).
				if (pkg != null && classname.startsWith(pkg)) {
					continue;
				}
				int bits = me.getValue();
				try {
					ClassLoader cl = SpringLoadedPreProcessor.class.getClassLoader();
					if (cl == null) {
						cl = ClassLoader.getSystemClassLoader();
					}
					if (cl == null) {
						throw new IllegalStateException(
								"Unable to determine a classloader to use to ensure system classes properly initialized for reloading");
					}
					Class<?> clazz = cl.loadClass(classname.replace('/', '.'));
					injectReflectiveInterceptorMethods(slashedClassName, bits, clazz);
					toRemoveList.add(classname);
				}
				catch (ClassCircularityError cce) {
					// See comment above. 'assume' this is OK, the initialization will happen again next time around.
				}
				catch (Exception e) {
					// NPE seen out in the wild:
					// java.lang.NullPointerException
					// at java.lang.reflect.Proxy.isProxyClass(Proxy.java:789)
					// at java.lang.Class.checkPackageAccess(Class.java:2358)
					// at java.lang.Class.checkMemberAccess(Class.java:2338)
					// at java.lang.Class.getDeclaredField(Class.java:2054)
					// at org.springsource.loaded.agent.SpringLoadedPreProcessor.injectReflectiveInterceptorMethods(SpringLoadedPreProcessor.java:446)
					// at org.springsource.loaded.agent.SpringLoadedPreProcessor.tryToEnsureSystemClassesInitialized(SpringLoadedPreProcessor.java:386)
					// at org.springsource.loaded.agent.SpringLoadedPreProcessor.preProcess(SpringLoadedPreProcessor.java:132)
					// at org.springsource.loaded.agent.ClassPreProcessorAgentAdapter.transform(ClassPreProcessorAgentAdapter.java:107)
					// at sun.instrument.TransformerManager.transform(TransformerManager.java:188)
					// at sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:428)
					// at java.lang.reflect.Proxy.<clinit>(Proxy.java:240)
					// at sun.reflect.misc.ReflectUtil.isNonPublicProxyClass(ReflectUtil.java:289)
					StringWriter sw = new StringWriter();
					e.printStackTrace(new PrintWriter(sw));
					String stringifiedException = sw.toString();
					if (stringifiedException.length() > 500)
						stringifiedException = stringifiedException.substring(0, 500);
					if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
						log.info("Exception occurred whilst trying to initalize system classes (probably harmless): "
								+ stringifiedException);
					}
				}
			}
			for (String toRemove : toRemoveList) {
				systemClassesRequiringInitialization.remove(toRemove); // TODO threads?
			}
		}
	}

	// TODO should cache these retrieved fields/methods for injection into types
	/**
	 * This method tries to inject the ReflectiveInterceptor methods into any system types that have been rewritten.
	 */
	private void injectReflectiveInterceptorMethods(String slashedClassName, int bits, Class<?> clazz)
			throws NoSuchFieldException,
			IllegalAccessException, NoSuchMethodException {
		// TODO log the bits
		if ((bits & Constants.JLC_GETDECLAREDFIELDS) != 0) {
			Field f = clazz.getDeclaredField("__sljlcgdfs");
			f.setAccessible(true);
			f.set(null, method_jlcgdfs);
		}
		if ((bits & Constants.JLC_GETDECLAREDFIELD) != 0) {
			Field f = clazz.getDeclaredField(jlcgdf);
			f.setAccessible(true);
			f.set(null, method_jlcgdf);
		}
		if ((bits & Constants.JLC_GETFIELD) != 0) {
			Field f = clazz.getDeclaredField(jlcgf);
			f.setAccessible(true);
			f.set(null, method_jlcgf);
		}
		if ((bits & Constants.JLC_GETDECLAREDMETHODS) != 0) {
			Field f = clazz.getDeclaredField(jlcgdms);
			f.setAccessible(true);
			f.set(null, method_jlcgdms);
		}
		if ((bits & Constants.JLC_GETDECLAREDMETHOD) != 0) {
			Field f = clazz.getDeclaredField(jlcgdm);
			f.setAccessible(true);
			f.set(null, method_jlcgdm);
		}
		if ((bits & Constants.JLC_GETMETHOD) != 0) {
			Field f = clazz.getDeclaredField(jlcgm);
			f.setAccessible(true);
			f.set(null, method_jlcgm);
		}
		if ((bits & Constants.JLC_GETDECLAREDCONSTRUCTOR) != 0) {
			Field f = clazz.getDeclaredField(jlcgdc);
			f.setAccessible(true);
			f.set(null, method_jlcgdc);
		}
		if ((bits & Constants.JLC_GETMODIFIERS) != 0) {
			Field f = clazz.getDeclaredField(jlcgmods);
			f.setAccessible(true);
			f.set(null, method_jlcgmods);
		}
		if ((bits & Constants.JLC_GETMETHODS) != 0) {
			Field f = clazz.getDeclaredField(jlcgms);
			f.setAccessible(true);
			f.set(null, method_jlcgms);
		}
		if ((bits & Constants.JLC_GETCONSTRUCTOR) != 0) {
			Field f = clazz.getDeclaredField(jlcgc);
			f.setAccessible(true);
			f.set(null, method_jlcgc);
		}
		if ((bits & Constants.JLC_GETDECLAREDCONSTRUCTORS) != 0) {
			Field f = clazz.getDeclaredField(jlcGetDeclaredConstructorsMember);
			f.setAccessible(true);
			f.set(null, method_jlcgdcs);
		}
		if ((bits & Constants.JLRF_GET) != 0) {
			Field f = clazz.getDeclaredField(jlrfGetMember);
			f.setAccessible(true);
			f.set(null, method_jlrfg);
		}
		if ((bits & Constants.JLRF_GETLONG) != 0) {
			Field f = clazz.getDeclaredField(jlrfGetLongMember);
			f.setAccessible(true);
			f.set(null, method_jlrfgl);
		}
		if ((bits & Constants.JLRM_INVOKE) != 0) {
			Field f = clazz.getDeclaredField(jlrmInvokeMember);
			f.setAccessible(true);
			f.set(null, method_jlrmi);
		}
		if ((bits & Constants.JLOS_HASSTATICINITIALIZER) != 0) {
			Field f = clazz.getDeclaredField(jloObjectStream_hasInitializerMethod);
			f.setAccessible(true);
			f.set(null, method_jloObjectStream_hasInitializerMethod);
		}
	}

	private static final Class<?> EMPTY_CLASS_ARRAY_CLAZZ = Class[].class;

	// TODO threads
	private static boolean prepared = false;

	private static Method method_jlcgdfs, method_jlcgdf, method_jlcgf, method_jlcgdms, method_jlcgdm, method_jlcgm,
			method_jlcgdc,
			method_jlcgc, method_jlcgmods, method_jlcgms, method_jlcgdcs, method_jlrfg, method_jlrfgl, method_jlrmi,
			method_jloObjectStream_hasInitializerMethod;

	/**
	 * Cache the Method objects that will be injected.
	 */
	private void ensurePreparedForInjection() {
		if (!prepared) {
			try {
				Class<ReflectiveInterceptor> clazz = ReflectiveInterceptor.class;
				method_jlcgdfs = clazz.getDeclaredMethod("jlClassGetDeclaredFields", Class.class);
				method_jlcgdf = clazz.getDeclaredMethod("jlClassGetDeclaredField", Class.class, String.class);
				method_jlcgf = clazz.getDeclaredMethod("jlClassGetField", Class.class, String.class);
				method_jlcgdms = clazz.getDeclaredMethod("jlClassGetDeclaredMethods", Class.class);
				method_jlcgdm = clazz.getDeclaredMethod("jlClassGetDeclaredMethod", Class.class, String.class,
						EMPTY_CLASS_ARRAY_CLAZZ);
				method_jlcgm = clazz.getDeclaredMethod("jlClassGetMethod", Class.class, String.class,
						EMPTY_CLASS_ARRAY_CLAZZ);
				method_jlcgdc = clazz.getDeclaredMethod("jlClassGetDeclaredConstructor", Class.class,
						EMPTY_CLASS_ARRAY_CLAZZ);
				method_jlcgc = clazz.getDeclaredMethod("jlClassGetConstructor", Class.class, EMPTY_CLASS_ARRAY_CLAZZ);
				method_jlcgmods = clazz.getDeclaredMethod("jlClassGetModifiers", Class.class);
				method_jlcgms = clazz.getDeclaredMethod("jlClassGetMethods", Class.class);

				method_jlcgdcs = clazz.getDeclaredMethod("jlClassGetDeclaredConstructors", Class.class);
				method_jlrfg = clazz.getDeclaredMethod("jlrFieldGet", Field.class, Object.class);
				method_jlrfgl = clazz.getDeclaredMethod("jlrFieldGetLong", Field.class, Object.class);
				method_jlrmi = clazz.getDeclaredMethod("jlrMethodInvoke", Method.class, Object.class, Object[].class);
				method_jloObjectStream_hasInitializerMethod = clazz.getDeclaredMethod("jlosHasStaticInitializer",
						Class.class);
			}
			catch (NoSuchMethodException nsme) {
				// cant happen, a-hahaha
				throw new Impossible(nsme);
			}
			prepared = true;
		}
	}

	private static boolean needsClientSideRewriting(String slashedClassName) {
		if (slashedClassName != null && slashedClassName.charAt(0) == 'o'
				&& slashedClassName.startsWith("org/springsource/loaded")) {
			return false;
		}
		return true;
	}

	/**
	 * Determine where to watch for changes based on the protectionDomain. Relying on the protectionDomain may prove
	 * fragile though, as it is up to the classloader in question to create it. Some classloaders will create one
	 * protectionDomain per 'directory' containing class files (and so the slashedClassName must be appended to the
	 * codesource). Some classloaders have a protectiondomain per class.
	 *
	 * @param protectionDomain the protection domain passed in to the defineclass call
	 * @param slashedClassName the slashed class name currently being defined
	 * @return the path to watch for changes to this class
	 */
	private String getWatchPathFromProtectionDomain(ProtectionDomain protectionDomain, String slashedClassName) {
		String watchPath = null;
		//		System.err.println("protectionDomain=" + protectionDomain + " slashedClassName=" + slashedClassName + " protdom="
		//				+ protectionDomain + " codesource=" + (protectionDomain == null ? "null" : protectionDomain.getCodeSource()));
		if (protectionDomain == null) {
			if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.WARNING)) {
				log.warning("Changes to type cannot be tracked: " + slashedClassName + " - no protection domain");
			}
		}
		else {
			try {
				CodeSource codeSource = protectionDomain.getCodeSource();
				if (codeSource == null || codeSource.getLocation() == null) {
					if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.WARNING)) {
						log.warning("null codesource for " + slashedClassName);
					}
				}
				else {
					if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.FINEST)) {
						log.finest("Codesource.getLocation()=" + codeSource.getLocation());
					}
					// A 'URI is not hierarchical' message can come out when the File ctor is called. Cases seen
					// so far:
					// GRAILS-10384: relative URL file:../foo/bar - should have built it with new File().toURI.toURL() and not just new URL()
					File file = null;
					URI uri = null;
					try {
						file = new File(codeSource.getLocation().getFile());
						uri = file.toURI();
					}
					catch (IllegalArgumentException iae) {
						boolean recovered = false;
						if (iae.toString().indexOf("URI is not hierarchical") != -1) {
							// try another approach...
							String uristring = uri.toString();
							if (uristring.startsWith("file:../")) {
								file = new File(uristring.substring(8)).getAbsoluteFile();
							}
							else if (uristring.startsWith("file:./")) {
								file = new File(uristring.substring(7)).getAbsoluteFile();
							}
							if (file != null && file.exists()) {
								recovered = true;
							}
						}
						if (!recovered) {
							System.out.println("Unable to watch file: classname = " + slashedClassName
									+ " codesource location = " + codeSource.getLocation() + " ex = " + iae.toString());
							return null;
						}
					}
					if (file.isDirectory()) {
						file = new File(file, slashedClassName + ".class");
					}
					else if (file.getName().endsWith(".class")) {
						// great! nothing to do
					}
					else if (file.getName().endsWith(".jar")) {
						boolean found = false;
						if (GlobalConfiguration.jarsToWatch != null) {
							// Check if it is one to watch
							String candidate = file.getName();
							for (String jarToWatch : GlobalConfiguration.jarsToWatch) {
								if (candidate.equals(jarToWatch)) {
									found = true;
									break;
								}
							}
						}
						if (!found && GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.WARNING)) {
							log.warning("unable to watch this jar file entry: " + slashedClassName.replace('/', '.')
									+ ". Computed location=" + file.toString());
						}
						if (!found)
							return null;
					}
					else if (file.toString().equals("/groovy/script") || file.toString().equals("\\groovy\\script")) {
						// nothing to do, compiled/loaded by a GroovyClassLoader$InnerLoader - there is nothing to watch.  If the type is to be
						// reloaded we will have to be told via an alternate route
						return null;
					}
					else if (!file.toString().endsWith(".class")) {
						// GRAILS-9076: it ended in .groovy
						// GRAILS-9069/GRAILS-9070: it was /groovy/shell
						// something other than a class, no point in watching it
						return null;
					}
					else {
						throw new UnsupportedOperationException("unable to watch " + slashedClassName.replace('/', '.')
								+ ". Computed location=" + file.toString());
					}
					watchPath = file.toString();
					if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) {
						log.info("Watched location for changes to " + slashedClassName + " is " + watchPath);
					}
				}
			}
			catch (Exception e) {
				throw new IllegalStateException("Unexpected problem processing URI ", e);
			}
		}
		return watchPath;
	}

	private void logPreProcess(ClassLoader classLoader, String slashedClassName, TypeRegistry typeRegistry) {
		String clname = classLoader == null ? "null" : classLoader.getClass().getName();
		if (clname.indexOf('.') != -1) {
			clname = clname.substring(clname.lastIndexOf('.') + 1);
		}
		log.info("SpringLoaded preprocessing: classname=" + slashedClassName + " classloader=" + clname
				+ " typeRegistry=" + typeRegistry);
	}

	public static List<Plugin> getGlobalPlugins() {
		if (plugins == null) {
			plugins = new ArrayList<Plugin>();
			// Ordering is important here (for some of the plugins) - try to do the lowest level things first in case the higher level
			// operations cause something to happen that will drive the lower level function.  For example, the JVM plugin clears the
			// Introspector class which is used by the Spring CachedIntrospectionResults class, which is used by the Grails ClassPropertyFetcher (
			// through its calls to BeanUtils).  If you don't clear the lower level things first then the higher level reinit operations will
			// still see the old (incorrect) results.
			plugins.add(new JVMPlugin());
			plugins.add(new SpringPlugin());
			plugins.add(new GroovyPlugin());
			plugins.add(new CglibPlugin());
			// Not used right now, grails mechanisms are clearing the state that this plugin is trying to
			// plugins.add(new GrailsPlugin());
			List<String> extraGlobalPlugins = GlobalConfiguration.pluginClassnameList;
			if (extraGlobalPlugins != null) {
				for (String globalPlugin : extraGlobalPlugins) {
					try {
						Class<?> pluginClass = Class.forName(globalPlugin, false,
								SpringLoadedPreProcessor.class.getClassLoader());
						plugins.add((Plugin) pluginClass.newInstance());
					}
					catch (ClassNotFoundException e) {
						System.err.println("Unexpected problem loading global plugin:" + globalPlugin);
						e.printStackTrace(System.err);
					}
					catch (InstantiationException e) {
						System.err.println("Unexpected problem loading global plugin:" + globalPlugin);
						e.printStackTrace(System.err);
					}
					catch (IllegalAccessException e) {
						System.err.println("Unexpected problem loading global plugin:" + globalPlugin);
						e.printStackTrace(System.err);
					}
				}
			}
		}
		return plugins;
	}

	private static List<IsReloadableTypePlugin> isReloadableTypePlugins = null;

	public static List<IsReloadableTypePlugin> getIsReloadableTypePlugins() {
		if (isReloadableTypePlugins == null) {
			synchronized (SpringLoadedPreProcessor.class) {
				if (isReloadableTypePlugins == null) {
					isReloadableTypePlugins = new ArrayList<IsReloadableTypePlugin>();
					for (Plugin p : getGlobalPlugins()) {
						if (p instanceof IsReloadableTypePlugin) {
							isReloadableTypePlugins.add((IsReloadableTypePlugin) p);
						}
					}
				}
			}
		}
		return isReloadableTypePlugins;
	}

	public static void registerGlobalPlugin(Plugin instance) {
		getGlobalPlugins(); // trigger initialization
		plugins.add(instance);
		isReloadableTypePlugins = null; // reset this cached value
	}

	public static void unregisterGlobalPlugin(Plugin instance) {
		getGlobalPlugins(); // trigger initialization
		plugins.remove(instance);
		isReloadableTypePlugins = null; // reset this cached value
	}
}
